/* HttpAcceptor - Accepts incomming http connections.
   Copyright (C) 2003 Mark J. Wielaard

   This file is part of Snark.
   
   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 2, or (at your option)
   any later version.
 
   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.
 
   You should have received a copy of the GNU General Public License
   along with this program; if not, write to the Free Software Foundation,
   Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
 */

package org.klomp.snark;

import java.io.*;
import java.net.*;
import java.util.*;

public class HttpAcceptor {
	private static final String SNARKHTML = "<?xml version=\"1.0\" encoding=\"iso-8859-1\"?>"
			+ "<!DOCTYPE html PUBLIC \"-//W3C//DTD HTML 4.0 Transitional//EN\""
			+ "\"http://www.w3.org/TR/html4/loose.dtd\">"
			+ "<html>"
			+ "<head><title>Snark Client</title></head>"
			+ "<body>"
			+ "<h1>Snark Client</h1>"
			+ "<p>Snark is a client for downloading and sharing files distributed with the BitTorrent protocol. It is not a normal webserver.</p>"
			+ "<p><a href=\"metainfo.torrent\">Torrent</a></p>"
			+ "<p><a href=\"announce\">Tracker</a></p>"
			+ "<hr><p>For more info see <a href=\"http://www.klomp.org/snark/\">The Hunting of the Snark Project</a></p>"
			+ "</body>" + "</html>";
	private static final byte[] SNARKPAGE;

	private static final String ASCII = "US-ASCII";
	private static final byte[] CRLF = new byte[] { '\r', '\n' };
	private static final byte[] HTTP_STATUS;
	private static final byte[] OK_STATUS;
	private static final byte[] CONTENT_LENGTH;
	private static final byte[] CONTENT_TYPE;

	static {
		try {
			SNARKPAGE = SNARKHTML.getBytes(ASCII);

			String STATUS = "HTTP/1.0 ";
			OK_STATUS = (STATUS + "200 OK").getBytes(ASCII);
			HTTP_STATUS = STATUS.getBytes(ASCII);
			CONTENT_LENGTH = "Content-Length: ".getBytes(ASCII);
			CONTENT_TYPE = "Content-Type: ".getBytes(ASCII);
		} catch(UnsupportedEncodingException uee) {
			// Cannot happen, US-ASCII unknown?
			throw new InternalError(uee.toString());
		}
	}

	private final Tracker tracker;

	/**
	 * Creates a HttpAcceptor that can handle torrent metadata of the given
	 * Tracker.
	 */
	public HttpAcceptor(Tracker tracker) {
		this.tracker = tracker;
	}

	public void connection(Socket sock, BufferedInputStream bis, BufferedOutputStream bos) throws IOException {
		BufferedReader br = new BufferedReader(new InputStreamReader(bis, ASCII));

		String resource = readRequest(br);
		if(Snark.debug >= Snark.INFO)
			Snark.debug("HTTP request for: " + resource, Snark.INFO);
		if(resource != null) {
			Map headers = readHeaders(br);
			if(Snark.debug >= Snark.DEBUG) {
				Snark.debug(headers.toString(), Snark.DEBUG);
				Snark.debug("", Snark.DEBUG);
			}

			if(resource.equals("/"))
				sendData(bos, SNARKPAGE, "text/html");
			else if(resource.startsWith("/announce")) {
				Map params = parseParams(resource);
				byte[] response = tracker.handleRequest(sock.getInetAddress(), sock.getPort(), params);
				sendData(bos, response, "application/octet-stream");
			} else if(resource.equals("/metainfo.torrent")) {
				byte[] torrent = tracker.getMetaInfo().getTorrentData();
				sendData(bos, torrent, "application/x-bittorrent");
			} else
				sendError(bos, 404, "Snark Client. Not a real webserver.");
		} else
			sendError(bos, 500, "Snark Client. Not a real webserver.");

		sock.close();
	}

	/**
	 * Processes an incomming HTTP request. Only handles the most basic GET
	 * requests. Returns the (URLEncoded) requested resource or null if the
	 * request wasn't a valid GET request.
	 */
	private static String readRequest(BufferedReader br) throws IOException {
		String request = br.readLine();
		if(request != null && request.startsWith("GET ")) {
			String resource;
			int index = request.indexOf(' ', 4);
			if(index == -1)
				resource = request.substring(4);
			else
				resource = request.substring(4, index);
			return resource;
		} else
			return null;
	}

	/**
	 * Consumes all headers and puts them into a Map mapping header value to
	 * header key Strings.
	 */
	private static Map readHeaders(BufferedReader br) throws IOException {
		Map m = new HashMap();
		String header = br.readLine();
		while(header != null && header.length() != 0) {
			header = br.readLine();
			if(header != null && header.length() != 0) {
				int index = header.indexOf(": ");
				if(index != -1) {
					String key = header.substring(0, index);
					String value = header.substring(index + 2);
					m.put(key, value);
				}
			}
		}
		return m;
	}

	/**
	 * Sends a HTTP OK, the necessary headers and the data.
	 */
	private static void sendData(OutputStream out, byte[] data, String content_type) throws IOException {
		sendData(out, 200, "OK", data, content_type);
	}

	private static void sendData(OutputStream out, int responseCode, String reason, byte[] data, String content_type)
			throws IOException {
		if(Snark.debug >= Snark.DEBUG)
			Snark.debug(
					"HTTP/1.0 " + responseCode + " " + reason + " " + content_type + " (" + data.length + " bytes)",
					Snark.DEBUG);
		byte[] type = content_type.getBytes(ASCII);

		// Status line
		out.write(HTTP_STATUS);
		out.write(Integer.toString(responseCode).getBytes(ASCII));
		out.write(' ');
		out.write(reason.getBytes(ASCII));
		out.write(CRLF);

		// Entity headers
		out.write(CONTENT_LENGTH);
		out.write(Integer.toString(data.length).getBytes(ASCII));
		out.write(CRLF);

		out.write(CONTENT_TYPE);
		out.write(type);
		out.write(CRLF);

		// Start of data
		out.write(CRLF);

		out.write(data);
		out.flush();
	}

	private static void sendError(OutputStream out, int responseCode, String reason) throws IOException {
		sendData(out, responseCode, reason, reason.getBytes(ASCII), "text/plain");
	}

	/**
	 * Returns a key to value map of the GET request query string parameters. It
	 * expects a '?' and the urlencoded key=value pairs. Note that the key and
	 * value are NOT url decoded before putting in the paramaters map.
	 */
	private static Map parseParams(String request) {
		Map m = new HashMap();
		int index = request.indexOf('?');
		if(index != -1) {
			String params = request.substring(index + 1);
			StringTokenizer st = new StringTokenizer(params, "&");
			while(st.hasMoreTokens()) {
				String token = st.nextToken();
				index = token.indexOf('=');
				if(index != -1) {
					String key = token.substring(0, index);
					String value = token.substring(index + 1);
					m.put(key, value);
				}
			}
		}
		return m;
	}
}
